/**
 * API客户端类
 * 封装HTTP请求，提供统一的请求/响应处理
 * 这个类负责处理所有的HTTP请求，包括认证、错误处理、重试机制等
 */

// 导入配置文件和依赖
// 从配置文件导入API配置、HTTP状态码和错误消息
import { API_CONFIG, HTTP_STATUS, ERROR_MESSAGES } from './config.js';
// 导入toast通知组件，用于显示错误和成功消息
import { toast } from 'sonner';

// API客户端类定义
// 使用ES6 class语法定义，提供面向对象的API客户端
class ApiClient {
  // 构造函数，在创建ApiClient实例时自动调用
  constructor() {
    // 设置基础URL，从配置中获取API服务器地址
    this.baseURL = API_CONFIG.BASE_URL;

    // 设置请求超时时间，从配置中获取超时时间（毫秒）
    this.timeout = API_CONFIG.TIMEOUT;

    // 设置默认请求头，使用展开运算符复制配置中的头部信息
    // 这样可以避免直接修改原始配置对象
    this.headers = { ...API_CONFIG.HEADERS };

    // 从本地存储获取认证token
    // 调用getToken方法获取存储的token
    this.token = this.getToken();

    // 如果token存在，则在请求头中添加Authorization字段
    // 使用Bearer认证方式，这是JWT token的标准格式
    if (this.token) {
      // 模板字符串语法，将token插入到Bearer字符串中
      this.headers.Authorization = `Bearer ${this.token}`;
    }
  }

  /**
   * 获取存储的token
   * 优先从localStorage获取，如果没有则从sessionStorage获取
   * @returns {string|null} 返回存储的token，如果没有则返回null
   */
  getToken() {
    // 使用逻辑或运算符，优先返回localStorage中的token
    // 如果localStorage中没有，则返回sessionStorage中的token
    // 如果两者都没有，则返回null
    return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
  }

  /**
   * 设置认证token
   * 将token存储到本地存储中，并更新请求头
   * @param {string} token - 认证token字符串
   * @param {boolean} persistent - 是否持久化存储，true使用localStorage，false使用sessionStorage
   */
  setToken(token, persistent = true) {
    // 更新实例变量中的token
    this.token = token;

    // 如果token存在，则更新请求头
    if (token) {
      // 设置Authorization请求头，使用Bearer认证格式
      this.headers.Authorization = `Bearer ${token}`;

      // 根据persistent参数决定存储方式
      if (persistent) {
        // 持久化存储到localStorage，关闭浏览器后仍然保留
        localStorage.setItem('auth_token', token);
      } else {
        // 临时存储到sessionStorage，关闭浏览器标签页后清除
        sessionStorage.setItem('auth_token', token);
      }
    } else {
      // 如果token为空，则清除相关设置
      // 删除请求头中的Authorization字段
      delete this.headers.Authorization;

      // 清除localStorage中的token
      localStorage.removeItem('auth_token');

      // 清除sessionStorage中的token
      sessionStorage.removeItem('auth_token');
    }
  }

  /**
   * 清除认证信息
   * 清除token和相关的存储信息，用于用户登出
   */
  clearAuth() {
    // 调用setToken方法，传入null来清除所有认证信息
    this.setToken(null);
  }

  /**
   * 构建完整URL
   * 将基础URL、端点和路径参数组合成完整的请求URL
   * @param {string} endpoint - API端点路径，如'/projects/:id'
   * @param {object} params - URL路径参数对象，如{id: '123'}
   * @returns {string} 完整的URL字符串
   */
  buildURL(endpoint, params = {}) {
    // 使用模板字符串组合基础URL和端点
    // 如果endpoint以/开头，则直接拼接；否则会自动添加/
    let url = `${this.baseURL}${endpoint}`;

    // 替换路径参数
    // 遍历params对象的所有键，将endpoint中的:key替换为实际值
    Object.keys(params).forEach(key => {
      // 使用字符串的replace方法替换占位符
      // :${key}是模板字符串语法，生成如:id的占位符
      url = url.replace(`:${key}`, params[key]);
    });

    // 返回构建完成的URL
    return url;
  }

  /**
   * 构建查询参数字符串
   * 将参数对象转换为URL查询字符串格式
   * @param {object} params - 查询参数对象
   * @returns {string} 查询字符串，如'?page=1&size=10'
   */
  buildQueryString(params) {
    // 创建URLSearchParams实例，用于构建查询字符串
    // URLSearchParams会自动处理URL编码
    const searchParams = new URLSearchParams();

    // 遍历参数对象的所有键值对
    Object.keys(params).forEach(key => {
      // 获取当前键对应的值
      const value = params[key];

      // 检查值是否有效（不为null、undefined或空字符串）
      if (value !== null && value !== undefined && value !== '') {
        // 如果值是数组，则添加多个同名参数
        if (Array.isArray(value)) {
          // 遍历数组，为每个元素添加一个查询参数
          value.forEach(item => searchParams.append(key, item));
        } else {
          // 如果值是单个值，则直接添加
          searchParams.append(key, value);
        }
      }
    });

    // 将URLSearchParams转换为字符串
    const queryString = searchParams.toString();

    // 如果查询字符串不为空，则在前面添加?号
    // 否则返回空字符串（表示没有查询参数）
    return queryString ? `?${queryString}` : '';
  }

  /**
   * 处理HTTP响应
   * 根据响应内容类型解析数据，并处理错误状态
   * @param {Response} response - fetch API返回的Response对象
   * @returns {Promise<any>} 解析后的响应数据
   */
  async handleResponse(response) {
    // 获取响应头中的Content-Type信息
    // 用于判断响应数据的格式类型
    const contentType = response.headers.get('content-type');

    // 声明变量用于存储解析后的数据
    let data;

    // 根据Content-Type判断响应数据格式并相应解析
    if (contentType && contentType.includes('application/json')) {
      // 如果Content-Type包含'application/json'，则解析为JSON对象
      data = await response.json();
    } else if (contentType && contentType.includes('text/')) {
      // 如果Content-Type包含'text/'，则解析为文本字符串
      data = await response.text();
    } else {
      // 其他情况（如图片、文件等）解析为Blob对象
      data = await response.blob();
    }

    // 检查HTTP状态码是否表示成功
    // response.ok 等价于 status >= 200 && status < 300
    if (!response.ok) {
      // 如果状态码表示错误，则创建错误对象
      // 优先使用响应数据中的错误信息，否则使用默认错误消息
      // 处理后端 ApiResponse 格式：{ success: false, message: "...", error: {...} }
      const errorMessage = data?.message || data?.error?.message || data?.error || ERROR_MESSAGES.UNKNOWN_ERROR;
      const error = new Error(errorMessage);

      // 将HTTP状态码附加到错误对象上
      error.status = response.status;

      // 将原始响应数据附加到错误对象上，便于调试
      error.data = data;

      // 抛出错误，终止当前函数的执行
      throw error;
    }

    // 处理后端统一响应格式 ApiResponse<T> { success: true, data: T, message: "..." }
    // 如果响应是 ApiResponse 格式，自动提取 data 字段
    if (data && typeof data === 'object' && 'success' in data && 'data' in data) {
      // 检查 success 字段
      if (data.success === false) {
        // 如果 success 为 false，即使 HTTP 状态码是 200，也应该抛出错误
        const errorMessage = data.message || data.error?.message || ERROR_MESSAGES.UNKNOWN_ERROR;
        const error = new Error(errorMessage);
        error.status = response.status;
        error.data = data;
        throw error;
      }
      // 如果 success 为 true，返回 data 字段
      return data.data;
    }

    // 如果响应不是 ApiResponse 格式，直接返回原始数据
    return data;
  }

  /**
   * 处理API请求错误
   * 根据错误类型和状态码提供相应的错误处理和用户提示
   * @param {Error} error - 错误对象，包含status、data等属性
   * @param {object} options - 错误处理选项
   * @param {boolean} options.showToast - 是否显示错误提示，默认true
   * @param {boolean} options.silent - 是否静默处理（不显示toast），默认false
   * @returns {object} 包含错误信息的对象
   */
  handleError(error, options = {}) {
    // 使用解构赋值获取选项参数，并设置默认值
    // 如果options中没有对应属性，则使用默认值
    const { showToast = true, silent = false } = options;

    // 在控制台输出错误信息，便于开发调试
    console.error('API请求错误:', error);

    // 初始化错误消息为未知错误
    let message = ERROR_MESSAGES.UNKNOWN_ERROR;

    // 使用switch语句根据HTTP状态码处理不同错误类型
    switch (error.status) {
      case HTTP_STATUS.UNAUTHORIZED:
        // 401未授权错误：token过期或无效
        message = ERROR_MESSAGES.UNAUTHORIZED;
        // 清除本地存储的认证信息
        this.clearAuth();
        // 重定向到登录页面
        // 检查是否在浏览器环境中（避免SSR报错）
        if (typeof window !== 'undefined') {
          window.location.href = '/auth/signin';
        }
        break;
      case HTTP_STATUS.FORBIDDEN:
        // 403禁止访问错误：权限不足
        message = ERROR_MESSAGES.FORBIDDEN;
        break;
      case HTTP_STATUS.NOT_FOUND:
        // 404资源不存在错误
        message = ERROR_MESSAGES.NOT_FOUND;
        break;
      case HTTP_STATUS.UNPROCESSABLE_ENTITY:
        // 422数据验证错误
        message = ERROR_MESSAGES.VALIDATION_ERROR;
        break;
      case HTTP_STATUS.INTERNAL_SERVER_ERROR:
        // 500服务器内部错误
        message = ERROR_MESSAGES.SERVER_ERROR;
        break;
      default:
        // 其他错误类型，根据错误名称判断
        if (error.name === 'TypeError' && error.message.includes('fetch')) {
          // 网络连接错误
          message = ERROR_MESSAGES.NETWORK_ERROR;
        } else if (error.name === 'AbortError') {
          // 请求超时错误
          message = ERROR_MESSAGES.TIMEOUT;
        }
    }

    // 根据配置决定是否显示错误提示
    if (showToast && !silent) {
      // 使用toast组件显示错误消息
      toast.error(message);
    }

    // 返回错误信息对象，包含消息、状态码和原始数据
    return { message, status: error.status, data: error.data };
  }

  /**
   * 重试机制
   * 当请求失败时自动重试，使用指数退避算法
   * @param {Function} fn - 要重试的异步函数
   * @param {number} attempts - 当前尝试次数，默认从1开始
   * @returns {Promise<any>} 重试成功后的结果
   */
  async retry(fn, attempts = 1) {
    try {
      // 尝试执行传入的函数
      return await fn();
    } catch (error) {
      // 如果执行失败，检查是否应该重试
      // 条件1：当前尝试次数小于最大重试次数
      // 条件2：错误类型适合重试
      if (attempts < API_CONFIG.RETRY.maxAttempts && this.shouldRetry(error)) {
        // 计算重试延迟时间
        // 使用指数退避算法：delay * backoff^(attempts-1)
        // 第1次重试：1000 * 2^0 = 1000ms
        // 第2次重试：1000 * 2^1 = 2000ms
        // 第3次重试：1000 * 2^2 = 4000ms
        const delay = API_CONFIG.RETRY.delay * Math.pow(API_CONFIG.RETRY.backoff, attempts - 1);

        // 等待指定的延迟时间
        // 使用Promise包装setTimeout，使其可以await
        await new Promise(resolve => setTimeout(resolve, delay));

        // 递归调用自身，增加尝试次数
        return this.retry(fn, attempts + 1);
      }

      // 如果达到最大重试次数或错误不适合重试，则抛出错误
      throw error;
    }
  }

  /**
   * 判断是否应该重试请求
   * 只有特定类型的错误才适合重试，避免无效重试
   * @param {Error} error - 错误对象
   * @returns {boolean} 是否应该重试
   */
  shouldRetry(error) {
    // 判断错误类型，只有以下情况才重试：
    return (
      // 网络连接错误（TypeError通常表示fetch失败）
      error.name === 'TypeError' ||
      // 5xx服务器内部错误（服务器临时故障）
      error.status >= 500 ||
      // 408请求超时（服务器响应超时）
      error.status === 408 ||
      // 429请求过多（服务器限流，稍后重试可能成功）
      error.status === 429
    );
  }

  /**
   * 通用HTTP请求方法
   * 这是所有API请求的核心方法，处理请求构建、发送、响应处理和错误处理
   * @param {string} method - HTTP方法（GET、POST、PUT、DELETE等）
   * @param {string} endpoint - API端点路径
   * @param {object} options - 请求选项配置
   * @param {object} options.params - URL路径参数对象
   * @param {object} options.query - 查询参数对象
   * @param {any} options.data - 请求体数据
   * @param {object} options.headers - 额外的请求头
   * @param {number} options.timeout - 请求超时时间
   * @param {boolean} options.retry - 是否启用重试机制
   * @param {boolean} options.showToast - 是否显示错误提示
   * @param {boolean} options.silent - 是否静默处理错误
   * @returns {Promise<any>} 解析后的响应数据
   */
  async request(method, endpoint, options = {}) {
    // 使用解构赋值获取选项参数，并设置默认值
    const {
      params = {},           // URL路径参数，如{id: '123'}
      query = {},            // 查询参数，如{page: 1, size: 10}
      data = null,           // 请求体数据
      headers = {},          // 额外的请求头
      timeout = this.timeout, // 请求超时时间，默认使用实例的timeout
      retry = true,          // 是否启用重试机制
      showToast = true,      // 是否显示错误提示toast
      silent = false,        // 是否静默处理错误（不显示toast）
    } = options;

    // 定义内部请求函数，用于重试机制
    const requestFn = async () => {
      // 构建完整的请求URL
      // 1. 构建基础URL + 端点
      // 2. 替换路径参数（如:id）
      // 3. 添加查询参数（如?page=1&size=10）
      const url = this.buildURL(endpoint, params) + this.buildQueryString(query);

      // 构建fetch请求配置对象
      const config = {
        // 将HTTP方法转换为大写（GET、POST等）
        method: method.toUpperCase(),
        // 合并默认请求头和额外请求头
        // 使用展开运算符，后面的headers会覆盖前面的同名属性
        headers: { ...this.headers, ...headers },
        // 设置请求超时信号
        // AbortSignal.timeout()创建一个在指定时间后自动取消的信号
        signal: AbortSignal.timeout(timeout),
      };

      // 处理请求体数据（非GET请求）
      if (data && method.toUpperCase() !== 'GET') {
        if (data instanceof FormData) {
          // 如果是FormData（文件上传），删除Content-Type头
          // 让浏览器自动设置正确的Content-Type和boundary
          delete config.headers['Content-Type'];
          config.body = data;
        } else {
          // 其他情况，将数据序列化为JSON字符串
          config.body = JSON.stringify(data);
        }
      }

      // 在开发环境中输出请求日志，便于调试
      console.log(`API请求: ${method.toUpperCase()} ${url}`, data);

      // 发送HTTP请求
      const response = await fetch(url, config);

      // 处理响应数据
      return await this.handleResponse(response);
    };

    try {
      // 根据retry参数决定是否使用重试机制
      const result = retry ? await this.retry(requestFn) : await requestFn();

      // 在开发环境中输出响应日志，便于调试
      console.log(`API响应: ${method.toUpperCase()} ${endpoint}`, result);

      // 返回处理后的响应数据
      return result;
    } catch (error) {
      // 如果请求失败，调用错误处理方法
      return this.handleError(error, { showToast, silent });
    }
  }

  /**
   * GET请求方法
   * 用于获取数据，通常不包含请求体
   * @param {string} endpoint - API端点路径
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 响应数据
   */
  async get(endpoint, options = {}) {
    // 调用通用request方法，指定HTTP方法为GET
    return this.request('GET', endpoint, options);
  }

  /**
   * POST请求方法
   * 用于创建新资源，通常包含请求体数据
   * @param {string} endpoint - API端点路径
   * @param {any} data - 请求体数据
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 响应数据
   */
  async post(endpoint, data, options = {}) {
    // 调用通用request方法，指定HTTP方法为POST
    // 使用展开运算符将data添加到options中
    return this.request('POST', endpoint, { ...options, data });
  }

  /**
   * PUT请求方法
   * 用于完整更新资源，通常包含完整的请求体数据
   * @param {string} endpoint - API端点路径
   * @param {any} data - 请求体数据
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 响应数据
   */
  async put(endpoint, data, options = {}) {
    // 调用通用request方法，指定HTTP方法为PUT
    // 使用展开运算符将data添加到options中
    return this.request('PUT', endpoint, { ...options, data });
  }

  /**
   * PATCH请求方法
   * 用于部分更新资源，通常包含部分请求体数据
   * @param {string} endpoint - API端点路径
   * @param {any} data - 请求体数据
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 响应数据
   */
  async patch(endpoint, data, options = {}) {
    // 调用通用request方法，指定HTTP方法为PATCH
    // 使用展开运算符将data添加到options中
    return this.request('PATCH', endpoint, { ...options, data });
  }

  /**
   * DELETE请求方法
   * 用于删除资源，通常不包含请求体
   * @param {string} endpoint - API端点路径
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 响应数据
   */
  async delete(endpoint, options = {}) {
    // 调用通用request方法，指定HTTP方法为DELETE
    return this.request('DELETE', endpoint, options);
  }

  /**
   * 文件上传方法
   * 专门用于上传文件，自动构建FormData
   * @param {string} endpoint - 上传接口端点
   * @param {File|FileList|Array<File>|object} file - 要上传的文件
   * @param {object} options - 请求选项
   * @returns {Promise<any>} 上传结果
   */
  async uploadFile(endpoint, file, options = {}) {
    // 创建FormData对象，用于文件上传
    const formData = new FormData();

    // 根据文件类型进行不同处理
    if (file instanceof File) {
      // 单个文件：直接添加到FormData
      formData.append('file', file);
    } else if (Array.isArray(file)) {
      // 文件数组：为每个文件添加一个字段
      file.forEach((f, index) => {
        // 使用索引作为字段名的一部分，避免重复
        formData.append(`file${index}`, f);
      });
    } else {
      // 对象形式：遍历对象的键值对
      Object.keys(file).forEach(key => {
        // 将对象的每个属性作为FormData的字段
        formData.append(key, file[key]);
      });
    }

    // 调用POST请求上传文件
    return this.request('POST', endpoint, {
      // 展开原有选项
      ...options,
      // 设置请求体为FormData
      data: formData,
      // 合并请求头，删除Content-Type让浏览器自动设置
      headers: {
        // 不设置Content-Type，让浏览器自动设置正确的boundary
        ...options.headers,
      },
    });
  }
}

// 创建单例实例
// 在整个应用中共享同一个ApiClient实例，避免重复创建
// 单例模式确保所有模块使用相同的配置和状态
export const apiClient = new ApiClient();

// 导出类定义，以便需要时创建新的独立实例
// 例如：const customClient = new ApiClient();
export default ApiClient;
